package com.drakulo.games.ais.core;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;

import com.drakulo.games.ais.core.audio.SoundHelper;
import com.drakulo.games.ais.core.building.Building;
import com.drakulo.games.ais.core.building.BuildingHelper;
import com.drakulo.games.ais.core.building.BuildingType;
import com.drakulo.games.ais.core.delayed.BuildingAction;
import com.drakulo.games.ais.core.delayed.HexagonMapAction;
import com.drakulo.games.ais.core.delayed.PreparationAction;
import com.drakulo.games.ais.core.delayed.ResearchAction;
import com.drakulo.games.ais.core.delayed.ColonyAction;
import com.drakulo.games.ais.ui.TileHelper;

/**
 * <h1>The game engine</h1>
 * <p>
 * The game engine is the game's core. It handle all game logic. A call to the
 * run method run will handle a loop of all game logic. Time between calls must
 * be handled out of the engine.
 * </p>
 * 
 * @author Drakulo
 * 
 */
public final class GameEngine {
	/** Time since last update to perform a call to the game engine each second */
	private static int timeSinceGameUpdate = 0;

	/**
	 * private constructor to force static use
	 */
	private GameEngine() {
		// Nothing there
	}

	/**
	 * This method lunches the game logic. Called once per second. Handle heavy
	 * logic
	 */
	public static void run(int delta) {
		// First, handle the colony pool
		Colony colony = null;
		do {
			colony = GameData.popColonyFromPool();
			if (colony != null) {
				GameData.addColony(colony);
			}
		} while (colony != null);

		// A call to the game logic must be done only once per
		// second.
		if (timeSinceGameUpdate >= 1000) {
			timeSinceGameUpdate = 0;
			List<Colony> colonies = GameData.getColonies();
			// System.out.println("Colonies to update : " + colonies.size());
			for (Colony c : colonies) {
				// System.out.println("Update for colony : " + c.getName());
				// Construction / update process
				buildingProcess(c);

				// Robots progress
				robotProcess(c);

				// Special actions process
				specialActionProcess(c);

				// Production process
				productionProcess(c);

				// Research progression
				researchProcess(c);

				// 3. Diplomacy TODO
				// 4. Battles TODO
			}

			// Exploration actions
			explorationActionsProcess();
		} else {
			// Wait for next second
			timeSinceGameUpdate += delta;
		}

	}

	/**
	 * This method handle the robot progress
	 */
	private static void explorationActionsProcess() {
		List<HexagonMapAction> actions = GameData.getHexagonMapActions();
		List<HexagonMapAction> actionsToRemove = new ArrayList<HexagonMapAction>();
		for (HexagonMapAction action : actions) {
			action.step();
			if (action.isDone()) {
				// The action is done so we have to remove it from the list.
				actionsToRemove.add(action);
			}
		}

		// All actions were processed. If there is some finished actions, we
		// remove them from the action list
		if (!actionsToRemove.isEmpty()) {
			for (HexagonMapAction action : actionsToRemove) {
				actions.remove(action);
				action.runCallback();
			}
		}
	}

	/**
	 * This method handle the robot progress
	 */
	private static void specialActionProcess(Colony c) {
		List<ColonyAction> actions = c.getSpecialActions();
		List<ColonyAction> actionsToRemove = new ArrayList<ColonyAction>();
		for (ColonyAction action : actions) {
			action.step();
			if (action.isDone()) {
				// The action is done so we have to remove it from the list.
				actionsToRemove.add(action);
			}
		}

		// All actions were processed. If there is some finished actions, we
		// remove them from the action list
		if (!actionsToRemove.isEmpty()) {
			for (ColonyAction action : actionsToRemove) {
				actions.remove(action);
				action.runCallback();
			}
		}
	}

	/**
	 * This method handle the robot progress
	 */
	private static void robotProcess(Colony c) {
		List<PreparationAction> actions = c.getPreparationActions();
		List<PreparationAction> actionsToRemove = new ArrayList<PreparationAction>();
		for (PreparationAction action : actions) {
			action.step();
			if (action.isDone()) {
				// The action is done so we have to remove it from the list.
				actionsToRemove.add(action);

				// The preparation is done. The tile have to be modified
				TileHelper.setTile(action.getX(), action.getY(),
						TileHelper.BUILDABLE_TILE, c);
				c.releaseRobots(1);
			}
		}

		// All actions were processed. If there is some finished actions, we
		// remove them from the action list
		if (!actionsToRemove.isEmpty()) {
			for (PreparationAction action : actionsToRemove) {
				actions.remove(action);
				action.runCallback();
			}
		}
	}

	/**
	 * This method handle research steps
	 */
	private static void researchProcess(Colony c) {
		ResearchAction research = c.getResearch();
		// If the research is null, that's because the player didn't started a
		// research. In that case, there is nothing to do.
		if (research != null) {
			research.step();
			if (research.isDone()) {
				// The research is done.
				research.getTechnology().setOwned(true);
				c.setResearch(null);
			}
		}
	}

	/**
	 * This method handles building construction and update
	 */
	private static void buildingProcess(Colony c) {
		List<BuildingAction> actions = c.getBuildingActions();
		List<BuildingAction> actionsToRemove = new ArrayList<BuildingAction>();
		for (BuildingAction action : actions) {
			// For each action, we make a step
			action.step();
			if (action.isDone()) {
				// The action is done so we have to remove it from the list.
				actionsToRemove.add(action);

				// Robots associated to the construction / update have to be
				// released
				c.releaseRobots(action.getRobotsUsed());

				// We have to update the building list in the game data.
				Building building = action.getBuilding();

				if (action.isUpgrade()) {
					building.upgrade();// Uprade
					building.setUpgrading(false); // The upgrading is done
				} else {
					// If the created building is unique, a flag must be set
					if (BuildingType.RESEARCH_CENTER.equals(building.getType())) {
						c.setResearchCenterBuilt(true);
					}

					building.setUnderConstruction(false);

					// Creation
					c.addBuilding(building);
				}
				SoundHelper.playSound(SoundHelper.BUILDING_END);
			}
		}

		// All actions were processed. If there is some finished actions, we
		// remove them from the action list
		if (!actionsToRemove.isEmpty()) {
			for (BuildingAction action : actionsToRemove) {
				actions.remove(action);
				action.runCallback();
			}
		}
	}

	/**
	 * Runs the production / consumption process
	 */
	private static void productionProcess(Colony c) {
		// First we get the global modifiers : production - consumption
		Map<Resource, BigDecimal> modifiers = analyzeModifiers(c);
		if (hasNegativeModifiers(modifiers)) {
			if (storeCanAbsorbLoss(c, modifiers)) {
				// There is enough resources in store to absorb the production
				// loss. Resource amount in store will decrease
				applyModifiers(c, modifiers);
			} else {
				// There is not enough resources in store to absorb the loss. We
				// have a problem : not all buildings can run but some may.
				chooseAndRunBuildings();
			}
		} else {
			// There is only production, we can simply apply it
			applyModifiers(c, modifiers);
		}
	}

	/**
	 * Analyses the buildings production and consumption and stock it in a map.
	 * Key : the resource. Value : the production modifier
	 * 
	 * @return a map of modifiers (production - consumption)
	 */
	private static Map<Resource, BigDecimal> analyzeModifiers(Colony c) {
		Resource[] resources = Resource.values();
		Map<Resource, BigDecimal> analysis = new HashMap<Resource, BigDecimal>();
		BigDecimal value;
		for (Resource r : resources) {
			analysis.put(r, BigDecimal.ZERO);
		}

		// For each building we will retreive production and consumption
		List<Building> buildings = c.getBuildings();
		if (!buildings.isEmpty()) {
			int i = 0;
			i++;
		}
		for (Building b : buildings) {
			Map<Resource, BigDecimal> prod = BuildingHelper.getProductionOf(b);
			Map<Resource, BigDecimal> cons = BuildingHelper.getConsumptionOf(b);
			for (Resource r : resources) {
				// Production
				value = prod.get(r).divide(BigDecimal.valueOf(60));
				if (value != null) {
					analysis.put(r, analysis.get(r).add(value));
				}

				// Consumption
				value = cons.get(r).divide(BigDecimal.valueOf(60));
				if (value != null) {
					analysis.put(r, analysis.get(r).subtract(value));
				}
			}
		}
		return analysis;
	}

	/**
	 * Tests if there is a negative value in the modifiers map. That's a simple
	 * search of a negative value.
	 * 
	 * @param modifiers
	 *            the modifiers
	 * @return true if there is a negative value
	 */
	private static boolean hasNegativeModifiers(
			Map<Resource, BigDecimal> modifiers) {
		Set<Resource> resources = modifiers.keySet();
		BigDecimal value;
		for (Resource r : resources) {
			value = modifiers.get(r);
			if (value != null && value.signum() == -1) {
				return true;
			}
		}
		return false;
	}

	/**
	 * This method takes modifiers and apply them to the store. The store has
	 * space limits according to buildings specs. If the previous store amount
	 * with the modifiers exeeds the max storable space, then the production
	 * will be capped to the max storable value.
	 * 
	 * @param modifiers
	 *            The modifiers map
	 */
	private static void applyModifiers(Colony c,
			Map<Resource, BigDecimal> modifiers) {
		Set<Resource> resources = modifiers.keySet();
		Map<Resource, BigDecimal> storageSpace = getStoreSpace(c);
		BigDecimal maxSpace;
		BigDecimal amount;
		BigDecimal currentStock;
		for (Resource r : resources) {
			maxSpace = storageSpace.get(r);
			amount = modifiers.get(r);
			currentStock = c.getResource(r);
			if (NumberHelper.infEq(maxSpace, currentStock.add(amount))) {
				// Tere is no more space in store for this resource, so we cap
				// it to the max storable. However, if the command center is not
				// built, it means that the player is currently colonizing the
				// sector
				if (c.isCommandCenterBuilt()) {
					c.setResource(r, maxSpace);
				} else {
					// The command center is not built yet. Resources are not
					// modified
				}
			} else {
				// Enough space for the amount produced
				c.updateResource(r, amount);
			}
			c.setCurrentModifier(r, amount);
		}
	}

	/**
	 * Tests if there is enough resources in store to absorb the given modifiers
	 * loss
	 * 
	 * @param c
	 *            the colony
	 * @param modifiers
	 *            the modifiers map
	 * @return true if there is enough resources in store to absorb the loss
	 */
	private static boolean storeCanAbsorbLoss(Colony c,
			Map<Resource, BigDecimal> modifiers) {
		Resource[] resources = Resource.values();
		BigDecimal storeValue;
		BigDecimal modifierValue;
		for (Resource r : resources) {
			storeValue = c.getResource(r);
			modifierValue = modifiers.get(r);
			if (NumberHelper.inf(storeValue, modifierValue)) {
				// Store value is lesser than the modifier. Absorption is not
				// possible
				return false;
			}
		}
		return true;
	}

	/**
	 * This method has to choose wich buildings will run and wich will not. The
	 * choices will be done according different paramaters
	 */
	private static void chooseAndRunBuildings() {
		// The selection is made by
		// TODO
	}

	/**
	 * Calculate the total amount of store space provided by the buildings
	 * 
	 * @param colony
	 *            The colony
	 * @return a map describing the store space available for each resource
	 */
	private static Map<Resource, BigDecimal> getStoreSpace(Colony c) {
		Map<Resource, BigDecimal> storeSpace = new HashMap<Resource, BigDecimal>();
		Resource[] resources = Resource.values();
		// Initialize of storeSpace map
		for (Resource r : resources) {
			storeSpace.put(r, BigDecimal.ZERO);
		}

		BigDecimal space;
		List<Building> buildings = c.getBuildings();
		// For each building we'll check the space provided for each resource
		for (Building b : buildings) {
			for (Resource r : resources) {
				space = b.getStoreSpaceFor(r);
				storeSpace.put(r, storeSpace.get(r).add(space));
			}
		}
		return storeSpace;
	}
}
